{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "tropical-collar",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.44 s (started: 2021-08-18 23:17:04 +08:00)\n"
     ]
    }
   ],
   "source": [
    "# 自动计算cell的计算时间\n",
    "%load_ext autotime\n",
    "\n",
    "%matplotlib inline\n",
    "%config InlineBackend.figure_format='svg' #矢量图设置，让绘图更清晰"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "swiss-fluid",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "origin\tgit@github.com:ustchope/handson-ml2.git (fetch)\n",
      "origin\tgit@github.com:ustchope/handson-ml2.git (push)\n",
      "[master 5d5032d] 更新 ch15 #1  Aug 18, 2021\n",
      " 1 file changed, 66 insertions(+)\n",
      " create mode 100644 ch15.ipynb\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "To git@github.com:ustchope/handson-ml2.git\n",
      "   f9087be..5d5032d  master -> master\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.77 s (started: 2021-08-18 23:17:28 +08:00)\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "# 增加更新\n",
    "git add *.ipynb *.md\n",
    "\n",
    "git remote -v\n",
    "\n",
    "git commit -m '更新 ch15 #1  Aug 18, 2021'\n",
    "\n",
    "git push origin master"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "stylish-situation",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 11.8 s (started: 2021-08-18 23:17:32 +08:00)\n"
     ]
    }
   ],
   "source": [
    "#设置使用的gpu\n",
    "import tensorflow as tf\n",
    "\n",
    "gpus = tf.config.list_physical_devices(\"GPU\")\n",
    "\n",
    "if gpus:\n",
    "   \n",
    "    gpu0 = gpus[0] #如果有多个GPU，仅使用第0个GPU\n",
    "    tf.config.experimental.set_memory_growth(gpu0, True) #设置GPU显存用量按需使用\n",
    "    # 或者也可以设置GPU显存为固定使用量(例如：4G)\n",
    "    #tf.config.experimental.set_virtual_device_configuration(gpu0,\n",
    "    #    [tf.config.experimental.VirtualDeviceConfiguration(memory_limit=4096)]) \n",
    "    tf.config.set_visible_devices([gpu0],\"GPU\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "powerful-thread",
   "metadata": {},
   "source": [
    "# 使用 RNN 和 CNN 处理序列"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "rising-cathedral",
   "metadata": {},
   "source": [
    "击球手击球。外野手立即开始奔跑，预测球的轨迹。他跟踪它，调整他的动作，最后抓住它（在雷鸣般的掌声中）。预测未来是你一直在做的事情，无论你是在完成朋友的一句话，还是在期待早餐时咖啡的味道。在本章中，我们将讨论循环神经网络 (RNN)，这是一类可以预测未来的网络（当然，在一定程度上）。他们可以分析股票价格等时间序列数据，并告诉您何时买入或卖出。在自动驾驶系统中，他们可以预测汽车轨迹并帮助避免事故。更一般地说，它们可以处理任意长度的序列，而不是像我们目前考虑的所有网络那样处理固定大小的输入。例如，它们可以将句子、文档或音频样本作为输入，这使得它们对于自动翻译或语音到文本等自然语言处理应用非常有用。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "moving-peeing",
   "metadata": {},
   "source": [
    "在本章中，我们将首先了解 RNN 的基本概念以及如何使用时间反向传播来训练它们，然后我们将使用它们来预测时间序列。 之后我们将探讨 RNN 面临的两个主要困难：\n",
    "* 不稳定梯度（在第 11 章中讨论），可以使用各种技术来缓解，包括循环 dropout 和循环层归一化\n",
    "*（非常）有限的短期记忆，可以使用 LSTM 和 GRU 单元进行扩展"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "underlying-testament",
   "metadata": {},
   "source": [
    "RNN 并不是唯一能够处理序列数据的神经网络类型：对于小序列，常规密集网络可以做到这一点； 对于非常长的序列，例如音频样本或文本，卷积神经网络实际上也可以很好地工作。 我们将讨论这两种可能性，我们将通过实现 WaveNet 来结束本章：这是一种 CNN 架构，能够处理数万个时间步长的序列。 在第 16 章中，我们将继续探索 RNN，看看如何将它们用于自然语言处理，以及基于注意力机制的最新架构。 让我们开始吧！"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "expired-wedding",
   "metadata": {},
   "source": [
    "## 循环神经元和层"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "considered-western",
   "metadata": {},
   "source": [
    "到目前为止，我们一直专注于前馈神经网络，其中激活仅在一个方向上流动，从输入层到输出层（附录 E 中讨论了一些例外）。 循环神经网络看起来非常像前馈神经网络，除了它也有指向后的连接。 让我们看一下最简单的 RNN，它由一个神经元接收输入、产生输出并将该输出发送回自身组成，如图 15-1（左）所示。 在每个时间步长 t（也称为帧），这个循环神经元接收输入 x(t) 以及它自己来自前一个时间步长 y(t-1) 的输出。 由于在第一个时间步没有以前的输出，所以一般设置为 0。我们可以将这个微小的网络表示在时间轴上，如图 15-1（右）所示。 这称为随时间展开网络（它是每个时间步表示一次的相同循环神经元）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "occupational-agent",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtld2yoy0kj61bk0ik76j02.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "quantitative-nutrition",
   "metadata": {},
   "source": [
    "您可以轻松创建一层循环神经元。 在每个时间步 t，每个神经元都接收输入向量 x(t) 和来自前一个时间步 y(t–1) 的输出向量，如图 15-2 所示。 请注意，现在输入和输出都是向量（当只有一个神经元时，输出是标量）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "indie-nudist",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtld40uugyj61ca0jetbj02.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "united-questionnaire",
   "metadata": {},
   "source": [
    "每个循环神经元都有两组权重：一组用于输入 x(t)，另一组用于前一时间步长 y(t-1) 的输出。 我们称这些权重向量为 $w_x$ 和 $w_y$。 如果我们考虑整个循环层而不是一个循环神经元，我们可以将所有权重向量放在两个权重矩阵 $W_x$ 和 $W_y$ 中。 然后，整个循环层的输出向量几乎可以按照您的预期进行计算，如公式 15-1 所示（b 是偏置向量，$φ(·)$ 是激活函数（例如 ReLU1）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "interested-space",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtld61bzc3j61ag06i74w02.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "useful-gravity",
   "metadata": {},
   "source": [
    "与前馈神经网络一样，我们可以通过将时间步 t 的所有输入放入输入矩阵 X(t) 中（参见公式 15-2），一次性计算整个小批量的循环层输出。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "thirty-beaver",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtld6wtb51j61b20cemya02.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "accessible-arrow",
   "metadata": {},
   "source": [
    "在这个等式中：\n",
    "* $\\mathbf{Y}_{(t)}$ 是一个 $m \\times n$ 神经元矩阵，其中包含小批量中每个实例在时间步长 $t$ 时层的输出（$m$ 是小批量中的实例数，$n$ 个神经元是神经元数）。\n",
    "* $\\mathbf{X}_{(t)}$ 是一个 $m \\times n$ 输入矩阵，包含所有实例的输入（n 个输入是输入特征的数量）。\n",
    "* $\\mathbf{W}_x$ 是一个 $n_{inputs} \\times n_{neurons}$ 矩阵，包含当前时间步长输入的连接权重。\n",
    "* $\\mathbf{W}_y$ 是一个神经元 $\\times$ 神经元矩阵，包含前一时间步输出的连接权重。\n",
    "* $b$ 是包含每个神经元偏置项的大小神经元向量。\n",
    "* 权重矩阵 $\\mathbf{W}_x$ 和 $\\mathbf{W}_y$ 通常垂直串联成一个形状为 $(n_{inputs} + n_{neurons}) \\times n_{neurons}$ 的权重矩阵 $\\mathbf{W}$（参见公式 15-2 的第二行）。\n",
    "* 符号$[\\mathbf{X}_{(t)} \\space \\mathbf{Y}_{(t-1)}]$ 表示矩阵$\\mathbf{X}_{(t)}$ 和$\\mathbf{Y}_{(t-1)}$ 的水平串联。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "velvet-guidance",
   "metadata": {},
   "source": [
    "请注意，$\\mathbf{Y}_{(t)}$ 是 $\\mathbf{X}_{(t)}$ 和 $\\mathbf{Y}_{(t-1)}$ 的函数，它是 $\\mathbf{X}_{(t-1)}$ 和 $\\mathbf{Y}_{(t-2)}$ 的函数，它是 $\\mathbf{X}_{(t- 2)}$ 和 $\\mathbf{Y}_{(t–3)}$，依此类推。 这使得 $\\mathbf{Y}_{(t)}$ 成为自时间 $t = 0$ 起所有输入的函数（即 $\\mathbf{X}_{(0)}, \\mathbf{X}_{(1)}, \\cdots, \\mathbf{X}_{(t)}$）。 在第一个时间步 $t = 0$，没有先前的输出，因此通常假设它们全为零。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "peripheral-superior",
   "metadata": {},
   "source": [
    "### 记忆细胞"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "analyzed-adrian",
   "metadata": {},
   "source": [
    "由于循环神经元在时间步 t 的输出是先前时间步的所有输入的函数，因此您可以说它具有某种形式的记忆。 跨时间步长保留某种状态的神经网络的一部分称为记忆单元（或简称为单元）。 单个循环神经元或循环神经元层是一个非常基本的细胞，只能学习短模式（通常长约 10 步，但这取决于任务）。 在本章的后面，我们将研究一些更复杂、更强大的细胞类型，它们能够学习更长的模式（大约长 10 倍，但这同样取决于任务）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "manual-thesis",
   "metadata": {},
   "source": [
    "通常，单元格在时间步 $t$ 的状态，表示为 $\\mathbf{h}_{(t)}$（“$\\mathbf{h}$”代表“隐藏”），是该时间步的某些输入及其在前一时间步的状态的函数：$\\mathbf{h}_{(t)} = f(\\mathbf{h}_{(t–1)}, \\mathbf{x}_{(t)})$。 它在时间步长 $t$ 的输出，表示为 $\\mathbf{y}_{(t)}$ ，也是先前状态和当前输入的函数。 在我们目前讨论的基本单元的情况下，输出只是等于状态，但在更复杂的单元中情况并非总是如此，如图 15-3 所示。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "invisible-colony",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtldydoq3lj61bu0juwg602.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "musical-passage",
   "metadata": {},
   "source": [
    "### 输入和输出序列"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "naked-fundamentals",
   "metadata": {},
   "source": [
    "RNN 可以同时接受一系列输入并产生一系列输出（参见图 15-4 中左上角的网络）。 这种类型的序列到序列网络对于预测诸如股票价格之类的时间序列非常有用：您将过去 N 天的价格提供给它，并且它必须输出未来一天的价格（即，从 N – 1天前到明天）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "engaging-behalf",
   "metadata": {},
   "source": [
    "或者，您可以为网络提供一系列输入并忽略除最后一个之外的所有输出（参见图 15-4 中右上角的网络）。 换句话说，这是一个序列到向量的网络。 例如，您可以向网络提供与电影评论相对应的单词序列，网络将输出情绪分数（例如，从 –1 [仇恨] 到 +1 [爱]）。\n",
    "\n",
    "相反，您可以在每个时间步上一遍又一遍地向网络提供相同的输入向量，并让它输出一个序列（参见图 15-4 的左下角网络）。 这是一个向量到序列的网络。 例如，输入可以是图像（或 CNN 的输出），输出可以是该图像的标题。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "geographic-baseball",
   "metadata": {},
   "source": [
    "最后，你可以有一个序列到向量网络，称为编码器，然后是一个向量到序列网络，称为解码器（见图 15-4 右下角的网络）。 例如，这可用于将句子从一种语言翻译成另一种语言。 你会向网络输入一种语言的句子，编码器将这个句子转换成单个向量表示，然后解码器将这个向量解码成另一种语言的句子。 这种称为编码器-解码器的两步模型比尝试使用单个序列到序列 RNN（如左上角表示的那个）即时翻译要好得多：句子的最后一个词可以影响 翻译的第一个词，所以你需要等到你看到整个句子再翻译。 我们将在第 16 章中看到如何实现编码器-解码器（正如我们将看到的，它比图 15-4 中建议的要复杂一些）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "european-pattern",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtle1depv9j614c0t8jv202.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "elementary-spanish",
   "metadata": {},
   "source": [
    "听起来很有希望，但是如何训练循环神经网络呢？"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "partial-survival",
   "metadata": {},
   "source": [
    "## 训练 RNN"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "engaging-housing",
   "metadata": {},
   "source": [
    "要训练 RNN，诀窍是通过时间展开它（就像我们刚刚做的那样），然后简单地使用常规的反向传播（见图 15-5）。这种策略称为时间反向传播 (BPTT)。\n",
    "\n",
    "就像在常规的反向传播中一样，有第一次前向通过展开的网络（由虚线箭头表示）。然后使用成本函数 $C(\\mathbf{Y}_{(0)}, \\mathbf{Y}_{(1)}, \\cdots, \\mathbf{Y}_{(T)})$ 评估输出序列)（其中 $T$ 是最大时间步长）。请注意，此成本函数可能会忽略某些输出，如图 15-5 所示（例如，在序列到向量 RNN 中，除了最后一个输出之外，所有输出都将被忽略）。然后，该成本函数的梯度通过展开的网络（由实线箭头表示）向后传播。最后，使用 BPTT 期间计算的梯度更新模型参数。请注意，梯度通过成本函数使用的所有输出向后流动，而不仅仅是通过最终输出（例如，在图 15-5 中，成本函数是使用网络的最后三个输出 $\\mathbf{Y}_{(2)}$ 计算的， $\\mathbf{Y}_{(3)}$ 和 $\\mathbf{Y}_{(4)}$，所以梯度流经这三个输出，但不流经 $\\mathbf{Y}_{(0)}$ 和 $\\mathbf{Y}_{(1)}$)。此外，由于在每个时间步使用相同的参数 $\\mathbf{W}$ 和 $\\mathbf{b}$，反向传播将做正确的事情并在所有时间步上求和。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "relative-panic",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtle8jzjhvj614k0judhi02.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "brave-collapse",
   "metadata": {},
   "source": [
    "## 预测时间序列"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "atomic-avenue",
   "metadata": {},
   "source": [
    "假设您正在研究您网站上每小时的活跃用户数量，或您所在城市的每日温度，或您公司的财务状况，每季度使用多个指标进行测量。 在所有这些情况下，数据将是每个时间步长一个或多个值的序列。 这称为时间序列。 在前两个示例中，每个时间步长只有一个值，因此它们是单变量时间序列，而在财务示例中，每个时间步长有多个值（例如，公司的收入、债务等），因此是 多元时间序列。 一个典型的任务是预测未来值，这称为预测。 另一个常见的任务是填补空白：预测（或者说“postdict”）过去的缺失值。 这称为插补。 例如，图 15-6 显示了 3 个单变量时间序列，每个序列有 50 个时间步长，这里的目标是预测每个时间步长（由 X 表示）的值。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "functional-electric",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtleavei94j61440ecjtd02.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "interior-stable",
   "metadata": {},
   "source": [
    "为简单起见，我们使用由 `generate_time_series()` 函数生成的时间序列，如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "coated-duncan",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.01 ms (started: 2021-08-19 00:52:57 +08:00)\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "def generate_time_series(batch_size, n_steps):\n",
    "    freq1, freq2, offsets1, offsets2 = np.random.rand(4, batch_size, 1)\n",
    "    time = np.linspace(0, 1, n_steps)\n",
    "    series = 0.5 * np.sin((time - offsets1) * (freq1 * 10 + 10)) # wave 1\n",
    "    series += 0.2 * np.sin((time - offsets2) * (freq2 * 20 + 20)) # + wave 2\n",
    "    series += 0.1 * (np.random.rand(batch_size, n_steps) - 0.5) # + noise\n",
    "    return series[..., np.newaxis].astype(np.float32)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ambient-saudi",
   "metadata": {},
   "source": [
    "此函数根据请求创建尽可能多的时间序列（通过 batch_size 参数），每个序列的长度为 n_steps，并且每个序列中的每个时间步只有一个值（即，所有序列都是单变量）。 该函数返回一个形状为 [批量大小，时间步长，1] 的 NumPy 数组，其中每个序列是两个固定幅度但随机频率和相位的正弦波的总和，加上一点噪声。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "analyzed-remark",
   "metadata": {},
   "source": [
    "> 在处理时间序列（以及其他类型的序列，例如句子）时，输入特征通常表示为形状 [批量大小、时间步长、维数] 的 3D 数组，其中对于单变量时间序列维数为 1，对于多变量时间维数更多 系列。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "thick-dakota",
   "metadata": {},
   "source": [
    "现在让我们使用这个函数创建一个训练集、一个验证集和一个测试集："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "civil-chancellor",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 450 ms (started: 2021-08-19 00:52:59 +08:00)\n"
     ]
    }
   ],
   "source": [
    "n_steps = 50\n",
    "series = generate_time_series(10000, n_steps + 1)\n",
    "X_train, y_train = series[:7000, :n_steps], series[:7000, -1]\n",
    "X_valid, y_valid = series[7000:9000, :n_steps], series[7000:9000, -1]\n",
    "X_test, y_test = series[9000:, :n_steps], series[9000:, -1]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "indirect-canon",
   "metadata": {},
   "source": [
    "X_train 包含 7,000 个时间序列（即其形状为 [7000, 50, 1]），而 X_valid 包含 2,000 个（从第 7,000 个时间序列到第 8,999 个）和 X_test 包含 1,000 个（从第 9,000 个到第 9,999 个） . 由于我们想为每个系列预测单个值，因此目标是列向量（例如，y_train 的形状为 [7000, 1]）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "stopped-gibson",
   "metadata": {},
   "source": [
    "### 基线指标"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "close-disease",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python [conda env:tensorflow]",
   "language": "python",
   "name": "conda-env-tensorflow-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
